﻿<!DOCTYPE html>
<html>
<head>
    <title>jSQL Test Select</title>
    <script type="text/javascript" src="scripts/jSQL.js"></script>
</head>
<body>
    
    <div id="results"></div>

    <script type="text/javascript">

        jSQL.options.validate = true;
        jSQL.options.validateTableItems = true;

        var oq = outputQry;
        var results = document.getElementById("results");
        
        // Excuse the mess, they're in no particular order but most of the functionality is tested.
        document.addEventListener("DOMContentLoaded", function (event) {
            testDataStore();
            testIterate();
            testInvalidOrderByExpression();
            testSkipTakeTopBottom();
            testUserDefinedAgg();
            testValidateActions();
            testSelectDistinct();
            testJoinSelect();
            testGroupByExistsAndFirst();
            testTableExistsAndFirst();
            testAutoGenOfAggs();
            testCopy();
            testAggOfNulls();
            testParseTable();
            testJoinEarlyExit();
            testValidateColumnName();
            testStringSelector();
            testStringSelector2();
            testSelectAllAlias();
            testAggNoResults();
            testEmptyTables();
            testToScalar();
            testTableSelect();
            testOrderByNoEffectOriginal();
            testSelectAll();
            testAggNoGroupNoJoin();
            testAggNoGroupNoJoin(21);
            testAggNoGroupNoJoin(6);
            testAggNoGroupWithJoin();
            testAggNoGroupWithJoin(22);
            testAggNoGroupWithJoin(18);
            testAggGroupNoJoin();
            testAggGroupWithJoin();
            testGroupBy();
            testGroupByHaving();
            testCount();
            testCountAll();
            testInvalidStringExpression();
            testInvalidSelectorErrorRunTime();
            testInvalidSelectorErrorPreRunTime();
            testInvalidFilterPreRuntime();
            testInvalidFilterRuntime();
            testReturnConstant();
            testAggConstants();
            testStupidReturnObjectInString();
            // Used to ensure that the results stay the say when changes are made to the script.
            console.log(hashString(results.innerHTML));
        });

        function testDataStore() {
            logH("testDataStore()");

            var dataStore = (function () {
                return {
                    orders: (function () {
                        return {
                            getItems: function () {
                                return [{ id: 1 }, { id: 2 }, { id: 3 }];
                            }
                        };
                    })(),
                    products: (function () {
                        return {
                            getItems: function () {
                                return [{ id: "p1", price: 10.25, orderId: 1 }, { id: "p3", price: 5.5, orderId: 3 }];
                            }
                        };
                    })()
                };
            })();

            oq(jSQL.qry().from(dataStore.orders, "o").join(dataStore.products, "p", "x.p.orderId === x.o.id"));
        }

        function testIterate() {
            logH("testInvalidOrderByExpression()");

            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
            
            res = [];
            jSQL.qry().from(orders).iterate(function (item) {
                res.push(item);
            }, function () {
                log("iterate");
                oq(res);
            });

            res = [];
            jSQL.qry().from(orders).top(2, function (item) {
                res.push(item);
            }, function () {
                log("top(2)");
                oq(res);
            });

            res = [];
            jSQL.qry().from(orders).bottom(2, function (item) {
                res.push(item);
            }, function () {
                log("bottom(2)");
                oq(res);
            });

            res = [];
            jSQL.qry().from(orders).skipTake(1, function (item) {
                res.push(item);
            }, function () {
                log("skip(1)");
                oq(res);
            });

            res = [];
            jSQL.qry().from(orders).skipTake(1, 2, function (item) {
                res.push(item);
            }, function () {
                log("skiptake(1, 2)");
                oq(res);
            });

        }

        function testInvalidOrderByExpression() {
            logH("testInvalidOrderByExpression()");

            var orders = [{ id: 1 }, { id: 2 }];

            log("PARSE-TIME");
            try { oq(jSQL.qry().from(orders).orderByDesc()); } catch (e) { logError(e, "No orderBy expression specified."); }
            try { oq(jSQL.qry().from(orders).orderBy(999)); } catch (e) { logError(e, "orderBy constant."); }
            try { oq(jSQL.qry().from(orders).orderBy({ id: "x.id", all: "*" })); } catch (e) { logError(e, "orderBy object."); }

            log("EXEC-TIME");
            try { oq(jSQL.qry().from(orders).orderByDesc(function (x) { return { id: x.id }; })); } catch (e) { logError(e, "orderBy return obj."); }
            try { oq(jSQL.qry().from(orders, "o").orderBy("x.o.www")); } catch (e) { logError(e, "orderBy invalid non-existent prop."); }

        }

        function testSkipTakeTopBottom() {
            logH("testSkipTakeTopBottom()");

            var orders = [{ id: 1, qty: 10 }, { id: 2, qty: 20 }, { id: 3, qty: 30 }, { id: 4, qty: 40 },
                          { id: 5, qty: 50 }, { id: 6, qty: 60 }, { id: 7, qty: 70 }, { id: 8, qty: 80 }];
            var func = function (item) {
                log(JSON.stringify(item));
            };
            var qry = jSQL.qry().from(orders);

            log("ORIGINAL");
            oq(orders);

            log("skip 2");
            oq(qry.skipTake(2));
            log("skip 2 [Iterate]");
            qry.skipTake(2, func);

            log("skip 0, take 3");
            oq(qry.skipTake(0, 3));
            log("skip 0, take 3 [Iterate]");
            qry.skipTake(0, 3, func);

            log("top 3");
            oq(qry.top(3));
            log("top 3 [Iterate]");
            qry.top(3, func);

            log("bottom 3");
            oq(qry.bottom(3));
            log("bottom 3 [Iterate]");
            qry.bottom(3, func);


            log("INVALID ARGS")
            try { qry.skipTake(); } catch (e) { logError(e, "skipTake()"); }
            try { qry.skipTake(-1); } catch (e) { logError(e, "skipTake(-1)"); }
            try { qry.skipTake(true); } catch (e) { logError(e, "skipTake(true)"); }
            try { qry.skipTake(0, true); } catch (e) { logError(e, "skipTake(0, true)"); }
            try { qry.skipTake(1, 0); } catch (e) { logError(e, "skipTake(1, 0)"); }
            try { qry.skipTake([1, 2]); } catch (e) { logError(e, "skipTake([1,2])"); }
            try { qry.skipTake(1, [1, 2]); } catch (e) { logError(e, "skipTake(1, [1,2])"); }
            try { qry.skipTake(1, 1, null); } catch (e) { logError(e, "skipTake(1, 1, null)"); }
            try { qry.skipTake(1, 1, [1, 2]); } catch (e) { logError(e, "skipTake(1, 1, [1,2])"); }

            try { qry.top(); } catch (e) { logError(e, "top()"); }
            try { qry.top(0); } catch (e) { logError(e, "top(0)"); }
            try { qry.top(true); } catch (e) { logError(e, "top(0)"); }
            try { qry.top(1, null); } catch (e) { logError(e, "top(1, null)"); }
            try { qry.top(1, [1, 2]); } catch (e) { logError(e, "top(1, [1,2])"); }

            try { qry.bottom(); } catch (e) { logError(e, "bottom()"); }
            try { qry.bottom(0); } catch (e) { logError(e, "bottom(0)"); }
            try { qry.bottom(true); } catch (e) { logError(e, "bottom(0)"); }
            try { qry.bottom(1, null); } catch (e) { logError(e, "bottom(1, null)"); }
            try { qry.bottom(1, [1, 2]); } catch (e) { logError(e, "bottom(1, [1,2])"); }
        }

        function testUserDefinedAgg() {
            logH("testUserDefinedAgg()");

            var qry;
            var orderItems = [{ id: 1000, orderId: 10, quantity: 1 }, { id: 1001, orderId: 10, quantity: 2 }];

            oq(jSQL.qry().from(orderItems).agg("x => x.total / x.count", "x.quantity"));
            oq(jSQL.qry().from(orderItems).agg(function (x) { return x.total / x.count; }, "x.quantity"));

            // This reports as no selector because the selector is created before the function.
            try { oq(jSQL.qry().from(orderItems).agg()); } catch (e) { logError(e); }

            // Only need to test 1, string functions are tested extensively elsewhere.
            try { oq(jSQL.qry().from(orderItems).agg([1, 2], "x.quantity")); } catch (e) { logError(e); }

            log("LOOP TIME");
            try { oq(jSQL.qry().from(orderItems).agg(function (x) { return 0 / 0; }, "x.id")) } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orderItems).agg(function (x) { return 1 / 0; }, "x.id")) } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orderItems).agg(function (x) { return -1 / 0; }, "x.id")) } catch (e) { logError(e); }
        }

        function testValidateActions() {
            logH("testValidateActions()");

            var orders = [{ id: 1, age: 1 }, { id: 1, age: 2 },
                         { id: 2, age: 30 }, { id: 2, age: 30 }];

            try { oq(jSQL.qry().max(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).max(10).selectDistinct("x.id", "x.age")); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).selectDistinct("x.id", "x.age").max(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).select("x.id").max(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).select("x.id").selectDistinct("x.id")); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).selectDistinct("x.id").select("x.id")); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).select("x.id").groupBy("x.id")); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).groupBy("x.id").selectDistinct("x.id")); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).select("x.id").having(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).having(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).groupBy("x.id").having(10)); } catch (e) { logError(e); }
            try { oq(jSQL.qry().from(orders).select("*").having(10)); } catch (e) { logError(e); }
            log("Ensure having is OK when select does have aggregates:");
            oq(jSQL.qry().from(orders).max("x.id").having("x.max_id === 2"));
            oq(jSQL.qry().from(orders).select().max("x.id").having("x.max_id === 2"));
        }

        function testSelectDistinct() {
            logH("testSelectDistinct()");

            var orders = [{ id: 1, age: 1 }, { id: 1, age: 2 },
                          { id: 2, age: 30 }, { id: 2, age: 30 }];
            oq(jSQL.qry().from(orders).selectDistinct("x.id", "x.age"));
            oq(jSQL.qry().from(orders).selectDistinct("x.id"));
            oq(jSQL.qry().from(orders).selectDistinct("x.age"));
            oq(jSQL.qry().from(orders).selectDistinct("*"));
            oq(jSQL.qry().from(orders).selectDistinct("*", "x.id[ID]", "hello"));
        }

        function testJoinSelect() {
            logH("testJoinSelect()");

            var qry1;

            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }];
            var orderItems = [{ id: 10, orderId: 1, qty: 2, productId: "p1" }, { id: 11, orderId: 1, qty: 3, productId: "NO" },
                              { id: 20, orderId: 2, qty: 5, productId: "p1" }, { id: 21, orderId: 2, qty: 10, productId: "p2" },
                              { id: 1000, orderId: 99, qty: 6, productId: "p2" }, { id: 1001, orderId: 99, qty: 7, productId: "NO" }];
            var products = [{ id: "p1", price: 10.25 }, { id: "p2", price: 5.5 }, { id: "p3", price: 2.5 }];

            qry1 = jSQL.qry().from(orders, "o")
                       .join(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                       .join(products, "p", function (x) { return x.p.id === x.oi.productId; })
                       .select({
                           id: "x.o.id", orderItemId: "x.oi.id", orderId: "x.oi.orderId", productId: "x.oi.productId",
                           qty: "x.oi.qty", productId2: "x.p.id", price: "x.p.price",
                           total: function (x) { return x.p.id === null ? null : x.p.price * x.oi.qty }
                       });
            outputQry(qry1.toArray());

            qry1 = jSQL.qry().from(orders, "o")
                       .leftjoin(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                       .leftjoin(products, "p", function (x) { return x.p.id === x.oi.productId; })
                       .select({
                           id: "x.o.id", orderItemId: "x.oi.id", orderId: "x.oi.orderId", productId: "x.oi.productId",
                           qty: "x.oi.qty", productId2: "x.p.id", price: "x.p.price",
                           total: function (x) { return x.p.id === null ? null : x.p.price * x.oi.qty }
                       });
            outputQry(qry1.toArray());

            qry1 = jSQL.qry().from(orders, "o")
                       .leftjoin(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                       .join(products, "p", function (x) { return x.p.id === x.oi.productId; })
                       .select({
                           id: "x.o.id", orderItemId: "x.oi.id", orderId: "x.oi.orderId", productId: "x.oi.productId",
                           qty: "x.oi.qty", productId2: "x.p.id", price: "x.p.price",
                           total: function (x) { return x.p.id === null ? null : x.p.price * x.oi.qty }
                       });
            outputQry(qry1.toArray());

            qry1 = jSQL.qry().from(orders, "o")
                       .join(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                       .leftjoin(products, "p", function (x) { return x.p.id === x.oi.productId; })
                       .select({
                           id: "x.o.id", orderItemId: "x.oi.id", orderId: "x.oi.orderId", productId: "x.oi.productId",
                           qty: "x.oi.qty", productId2: "x.p.id", price: "x.p.price",
                           total: function (x) { return x.p.id === null ? null : x.p.price * x.oi.qty }
                       });
            outputQry(qry1.toArray());

        }

        function testGroupByExistsAndFirst() {
            logH("testExistsGroupBy()");

            var qry;
            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
            var orderItems = [{ id: 10, orderId: 1, qty: 5 }, { id: 11, orderId: 1, qty: 6 },
                              { id: 20, orderId: 2, qty: 100 },
                              { id: 30, orderId: 3, qty: 10 }, { id: 31, orderId: 3, qty: 1 }];

            qry = jSQL.qry().from(orders, "o")
                             .leftjoin(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                             .where(function (x) { return x.oi.id !== null })
                             .groupBy({ id: "x.o.id" }).sum({ totalQty: "x.oi.qty" });

            log("GROUPED:");
            outputQry(qry.toArray());
            log("HAVING totalQty === 11, FIRST & EXISTS:");
            qry = qry.having("x.totalQty === 11");
            log(JSON.stringify(qry.find()) + " " + qry.exists());
        }

        function testTableExistsAndFirst() {
            logH('testTableExistsAndFirst()');

            var qry;
            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
            var orderItems = [{ id: 10, orderId: 1 }, { id: 20, orderId: 2 }, { id: 20, orderId: 2 }, { id: 80, orderId: 8 }];

            function out(msg, q) {
                log(msg);
                log(JSON.stringify(q.find()) + " " + q.exists());
            }

            out("NO JOIN, NO ALIAS, NO WHERE: ", jSQL.qry().from(orders));
            out("NO JOIN, NO WHERE: ", jSQL.qry().from(orders, "o"));
            out("NO JOIN, NO ALIAS, +WHERE: ", jSQL.qry().from(orders).where(function (x) { return x.id == 2 }));
            out("NO JOIN, +WHERE: ", jSQL.qry().from(orders, "o").where(function (x) { return x.o.id == 2 }));
            out("+JOIN, NO WHERE: ", jSQL.qry().from(orders, "o")
                    .join(orderItems, "oi", function (x) { return x.oi.orderId == x.o.id; }));
            out("+JOIN, +WHERE: ", jSQL.qry().from(orders, "o")
                .join(orderItems, "oi", function (x) { return x.oi.orderId == x.o.id; })
                .where(function (x) { return x.o.id == 2 }));

            out("+SELECT, -JOIN, -ALIAS, -WHERE: ", jSQL.qry().from(orders).select("x.id"));
            out("+SELECT, -JOIN, -WHERE: ", jSQL.qry().from(orders, "o").select("x.o.id"));

            out("+SELECT, NO JOIN, NO ALIAS, +WHERE: ", jSQL.qry().from(orders).select("x.id")
                .where(function (x) { return x.id == 2 }));

            out("+SELECT, NO JOIN, +WHERE: ", jSQL.qry().from(orders, "o").select("x.o.id")
                .where(function (x) { return x.o.id == 2 }));

            out("+SELECT, +JOIN, NO WHERE: ", jSQL.qry().from(orders, "o")
                .join(orderItems, "oi", function (x) { return x.oi.orderId == x.o.id; })
                .select("x.o.id", "x.oi.id[oi_id]"));
            out("+SELECT, +JOIN, +WHERE: ", jSQL.qry().from(orders, "o")
                .join(orderItems, "oi", function (x) { return x.oi.orderId == x.o.id; })
                .select("x.o.id", "x.oi.id[oi_id]")
                .where(function (x) { return x.o.id == 2 }));
        }

        function testAutoGenOfAggs() {
            logH("testAutoGenOfAggs()");

            var t = [{ id: 1, age: 10 }, { id: 1, age: 11 }, { id: 2, age: 12 }, { id: 2, age: 13 }];

            var save = jSQL.options.aggregateAutoName;
            var qry = jSQL.qry().from(t).count("x.age").sum("x.age").min("x.age").avg("x.id * x.age");

            log("[jSQL.options.aggregateAutoName: true]");
            jSQL.options.aggregateAutoName = true;
            try { oq(qry); } catch (e) { logError(e); }

            log("[jSQL.options.aggregateAutoName: false]");
            jSQL.options.aggregateAutoName = false;
            oq(qry);

            log("[jSQL.options.aggregateAutoName: agg_name. Last column will not be auto'd because it is complex:]");
            jSQL.options.aggregateAutoName = "agg_name";
            oq(qry);

            qry.count("x.age");
            log("[Add another: count('x.age')]");
            try { oq(qry); } catch (e) { logError(e); }

            log("[DO THE SAME 4 ABOVE BUT WITH GROUPBY ID]");
            qry = jSQL.qry().from(t).groupBy("x.id").count("x.age").sum("x.age").min("x.age").avg("x.id * x.age");

            jSQL.options.aggregateAutoName = true;
            try { oq(qry); } catch (e) { logError(e); }

            jSQL.options.aggregateAutoName = false;
            oq(qry);

            jSQL.options.aggregateAutoName = "agg_name";
            oq(qry);

            qry.count("x.age");
            try { oq(qry); } catch (e) { logError(e); }

            jSQL.options.aggregateAutoName = save;
        }

        function testCopy() {
            logH("testCopy()");

            var o = [{ id: 1, price: 1.5 }, { id: 2, price: 4.5 }, { id: 3, price: 30 }, { id: 4, price: 40 }];
            var qry = jSQL.qry().from(o);
            log("original");
            oq(qry);
            log("copy & add selector to original: x.id == 1 || x.id == 4. original:");
            var qry2 = qry.extend();
            qry.where("x.id == 1 || x.id == 4");
            oq(qry);
            log("copy");
            oq(qry2);
            log("add selector to copy: x.id == 2 || x.id == 3. copy:");
            qry2.where("x.id == 2 || x.id == 3");
            oq(qry2);

            log("push on the table: { id: 1, price: 99 }, { id: 2, price: 99 }");
            o.push({ id: 1, price: 99 }, { id: 2, price: 99 });
            log("original");
            oq(qry);
            log("copy");
            oq(qry2);

        }

        function testAggOfNulls() {
            logH("testAggOfNulls()");
            var t = [{ id: 1, age: null }, { id: 1, age: null },
                     { id: 2, age: null }, { id: 2, age: null }];

            var qry1 = jSQL.qry().from(t).count(10).sum(10).count("x.age[count]").sum("x.age[sum]").min("x.age[min]").max("x.age[max]")
                                   .avg("x.age[avg]").variance("x.age[variance]").varp("x.age[varp]")
                                   .stdev("x.age[stdev]").stdevp("x.age[stdevp]");

            // All nulls
            oq(qry1);

            t.push({ id: 2, age: 10 });

            // 1 non null value
            oq(qry1);

            var qry2 = jSQL.qry().from(t).groupBy("x.id").count(10).sum(10).count("x.age[count]").sum("x.age[sum]").min("x.age[min]")
                                   .max("x.age[max]").avg("x.age[avg]").variance("x.age[variance]").varp("x.age[varp]")
                                   .stdev("x.age[stdev]").stdevp("x.age[stdevp]");

            // This group should be the same as the 2 total aggregates above
            oq(qry2);

            // One more test to make sure it's working...
            t.push({ id: 1, age: 12.17 });
            t.push({ id: 1, age: 50.32 });
            t.push({ id: 2, age: 25.45 });

            oq(qry1);
            oq(qry2);
        }

        function testStupidReturnObjectInString() {
            logH('testStupidReturnObjectInString()');
            var products = [{ id: "p1", name: "product 1", type: 11 }, { id: "p2", name: "product 2", type: 55 }];
            oq(jSQL.qry().from(products, "p").select("x.p.id[ProductId]", "{name: x.p.name, type: x.p.type}"));
        }

        function testJoinEarlyExit() {
            logH('testJoinEarlyExit()');
            var qry;
            var orders = [{ id: 1, address: "1 street" }, { id: 2, address: "2 street" },
                        { id: 3, address: "3 street" }, { id: 4, address: "4 street" }];
            var orderItems = [{ id: 10, orderId: 1, productId: "p1", qty: 2 },
                              { id: 11, orderId: 1, productId: "p3", qty: 3 },
                              { id: 20, orderId: 2, productId: "NoProduct", qty: 3 },
                              { id: 30, orderId: 3, productId: "p3", qty: 4 },
                              { id: 40, orderId: 4, productId: "NoProduct2", qty: 4 }
            ];
            var products = [{ id: "p1", name: "product 1", type: 11 }, { id: "dangled1", name: "dangled1", type: 22 },
                            { id: "p3", name: "product 3", type: 55 },
                            { id: "dangled2", name: "dangled2", type: 55 }];

            outputQry(["BEFORE"]);
            outputQry(products);

            qry = jSQL.qry().from(products, "p")
                       .leftjoin(orderItems, "oi", "x.oi.productId === x.p.id")
                       .leftjoin(orders, "o", "x.o.id === x.oi.orderId")
                       .where("x.o.id === null");

            oq([qry.find()]);

            outputQry(["AFTER"]);
            outputQry(products);
        }

        function testValidateColumnName() {
            logH('testValidateColumnName()');
            var oi = [{ id: 1, price: 1.5, qty: 10 }];

            try {
                oq(jSQL.qry().from(oi).select("x.price", "*"));
            } catch (e) { logError(e); }

            try {
                oq(jSQL.qry().from(oi, "oi").select("*", "oi.*"));
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(oi).select("x.price", "x.price * x.qty[price]").toArray();
            } catch (e) { logError(e); }

            // loop time
            try {
                oq(jSQL.qry().from(oi).select("x.price", function (x) { return { price: x.price * x.qty }; }).toArray());
            } catch (e) { logError(e); }

            // loop time - second selector name will be auto generated as "Column 1"
            try {
                oq(jSQL.qry().from(oi).select("x.price[Column 1]", function (x) { return x.price; }).toArray());
            } catch (e) { logError(e); }
        }

        function testInvalidStringExpression() {
            logH('testInvalidStringExpression()');
            var oi = [{ id: 1, price: 1.5, qty: 10 }];
            // ==> instead of =>
            try {
                jSQL.qry().from(oi, "oi").where("y ==> y.oi.id !== 99").toArray();
            } catch (e) { logError(e); }
            // Includes period in the argument
            try {
                jSQL.qry().from(oi, "oi").where("y => y.oi.id !== 99").groupBy("y => y.oi.id").sum("y.oi => y.oi.price").toArray();
            } catch (e) { logError(e); }
            // Includes return
            try {
                jSQL.qry().from(oi, "oi").where("y => return y.oi.id !== 99").toArray();
            } catch (e) { logError(e); }
            // Includes return
            try {
                jSQL.qry().from(oi, "oi").where("return x.oi.id !== 99").toArray();
            } catch (e) { logError(e); }
        }

        function testStringSelector() {
            logH('testStringSelector()');
            // 1

            var qry;
            var o = [{ id: 1, price: 1.5 }, { id: 2, price: 4.5 }, { id: 3, price: 30 }, { id: 4, price: 40 }];
            var oi = [{ id: 10, orderId: 1 }, { id: 20, orderId: 2 }, { id: 20, orderId: 2 }, { id: 80, orderId: 8 }];

            qry = jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                      .select("x.oi.id", "x.o.price", { total: function (x) { return x.o.id * x.o.price; } });
            oq(qry);

            log(qry.find(true));
            log(qry.find("id"));
            log(qry.find().id);

            oq(jSQL.qry().from(o, "o").groupBy("x.o.id").sum("x.o.price").having(function (x) { return x.price >= 30; }));


            //2

            qry = jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                      .select("x.oi.id[ORDERITEMID]", "x.o.price[PRICE]", { total: function (x) { return x.o.id * x.o.price; } });
            oq(qry);

            log(qry.find(true));
            log(qry.find("ORDERITEMID"));
            log(qry.find().ORDERITEMID);

            oq(jSQL.qry().from(o, "o").groupBy("x.o.id[ORDERITEMID]").sum("x.o.price[PRICE]").having(function (x) { return x.PRICE >= 30; }));
            oq(jSQL.qry().from(o, "o").groupBy("x.o.id[ORDERITEMID]").sum("x.o.price").having(function (x) { return x.price >= 30; }));
        }

        function testStringSelector2() {
            logH('testStringSelector2()');
            var o = [{ id: 1, price: 1.5, qty: 10 }, { id: 2, price: 4.5, qty: 11 }, { id: 3, price: 30, qty: 12 }];
            var oi = [{ id: 1, price: 1.5, qty: 10 }, { id: 1, price: 4.5, qty: 2 },
                      { id: 2, price: 30, qty: 12 }, { id: 2, price: 40, qty: 12 },
                      { id: 3, price: 15, qty: 1 }, { id: 3, price: 9, qty: 1 },
                      { id: 99, price: 1.5, qty: 10 }, { id: 99, price: 4.5, qty: 2 }];

            // Where + OrderBy
            oq(jSQL.qry().from(o).where("x.id === 1 || x.id === 3").orderByDesc("x.id"));
            oq(jSQL.qry().from(o).where("y => y.id === 1 || y.id === 3").orderByDesc("y => y.id"));
            oq(jSQL.qry().from(o, "o").where("x.o.id === 1 || x.o.id === 3").orderByDesc("x.o.id"));
            oq(jSQL.qry().from(o, "o").where("y => y.o.id === 1 || y.o.id === 3").orderByDesc("y => y.o   .id"));

            // Select
            oq(jSQL.qry().from(o).select("x.id", "x.qty * x.price[total]", "constant", "const[CONST]"));
            oq(jSQL.qry().from(o).select("y => y.id", "y => y.qty * y.price[total]", "constant", "const[CONST]"));
            oq(jSQL.qry().from(o, "o").select("x.o.id", "x.o.qty * x.o.price[total]", "constant", "const[CONST]", "o.*"));
            oq(jSQL.qry().from(o, "o").select("y => y.o.id", "y => y.o.qty * y.o.price[total]", "constant", "const[CONST]", "o.*"));

            // Object selector
            oq(jSQL.qry().from(o).select({
                id: "x.id", price: "x.price", qty: "x.qty", total: "x.qty * x.price", constant: "constant"
            }));
            oq(jSQL.qry().from(o).select({
                id: "y => y.id", price: "y => y.price", qty: "y => y.qty", total: "y => y.qty * y.price", constant: "constant"
            }));
            oq(jSQL.qry().from(o, "o").select({
                id: "x.o.id", price: "x.o.price", qty: "x.o.qty", total: "x.o.qty * x.o.price", constant: "constant"
            }));
            oq(jSQL.qry().from(o, "o").select({
                id: "y => y.o.id", price: "y => y.o.price", qty: "y => y.o.qty", total: "y => y.o.qty * y.o.price", constant: "constant"
            }));


            // Where + GroupBy + Having
            oq(jSQL.qry().from(oi).where("x.id !== 99")
                            .groupBy("x.id").sum("x.price").sum("x.qty").sum("x.price * x.qty[total]")
                            .having("x.total === 24"));
            oq(jSQL.qry().from(oi).where("y => y.id !== 99")
                            .groupBy("y => y.id").sum("y => y.price").sum("y => y.qty").sum("y => y.price * y.qty[total]")
                            .having("y => y.total === 24"));
            oq(jSQL.qry().from(oi, "oi").where("x.oi.id !== 99")
                                  .groupBy("x.oi.id").sum("x.oi.price").sum("x.oi.qty").sum("x.oi.price * x.oi.qty[total]")
                                  .having("x.total === 24"));
            oq(jSQL.qry().from(oi, "oi").where("y => y.oi.id !== 99")
                                  .groupBy("y => y.oi.id").sum("y => y.oi.price").sum("y => y.oi.qty")
                                  .sum("y => y.oi.price * y.oi.qty[total]").having("y => y.total === 24"));
        }

        function testSelectAllAlias() {
            logH('testSelectAllAlias()');

            var o = [{ id: 1, price: 10 }, { id: 2, price: 45 }, { id: 3, price: 30 }, { id: 4, price: 40 }];
            var oi = [{ id: 10, orderId: 1 }, { id: 20, orderId: 2 }, { id: 20, orderId: 2 }, { id: 80, orderId: 8 }];

            oq(jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                .select({ total: function (x) { return x.o.id * x.o.price; } }, "*"));

            oq(jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                .select({ total: function (x) { return x.o.id * x.o.price; } }, "oi.*"));

            oq(jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                .select("oi.*", { total: function (x) { return x.o.id * x.o.price; } }));

            oi = [{ id: 10, orderId: 999 }];

            oq(jSQL.qry().from(o, "o").leftjoin(oi, "oi", function (x) { return x.oi.orderId === x.o.id; })
                           .select({ total: function (x) { return x.o.id * x.o.price; } }, "oi.*"));

        }

        function testAggNoResults() {
            logH('testAggNoResults()');

            var o = [{ id: 1, price: 10 }, { id: 2, price: 20 }, { id: 3, price: 30 }];

            outputQry(jSQL.qry().from(o).count("x.id").sum("x.price"));
            outputQry(jSQL.qry().from(o).count("x.id").sum("x.price").where(function (x) { return x.id === 999; }));
        }

        function testEmptyTables() {
            logH('testEmptyTables()');

            var o = [{ id: 1 }, { id: 2 }, { id: 3 }];
            var oi = [{ id: 10, orderId: 1 }, { id: 20, orderId: 2 }, { id: 30, orderId: 3 }];
            var eo = [];
            var eoi = [];

            log("Neither empty");
            outputQry(jSQL.qry().from(o, "o").join(oi, "oi", function (x) { return x.o.id === x.oi.orderId; }));
            log("Both empty");
            outputQry(jSQL.qry().from(eo, "eo").join(eoi, "eoi", function (x) { return x.eo.id === x.oi.orderId; }));
            log("T1 empty");
            outputQry(jSQL.qry().from(eo, "eo").join(oi, "oi", function (x) { return x.eo.id === x.oi.orderId; }));
            log("Join empty");
            outputQry(jSQL.qry().from(o, "o").join(eoi, "eoi", function (x) { return x.o.id === x.oi.orderId; }));

            log("Both empty. LJoin");
            outputQry(jSQL.qry().from(eo, "eo").leftjoin(eoi, "eoi", function (x) { return x.eo.id === x.oi.orderId; }));
            log("T1 empty. LJoin");
            outputQry(jSQL.qry().from(eo, "eo").leftjoin(oi, "oi", function (x) { return x.eo.id === x.oi.orderId; }));
            log("Join empty. LJoin");
            outputQry(jSQL.qry().from(o, "o").leftjoin(eoi, "eoi", function (x) { return x.o.id === x.oi.orderId; }));
        }

        function testParseTable() {
            logH('testParseTable()');

            var tok = [{ id: 1, name: "1" }, { name: "2", id: 2 }];
            var tbad = [{ id: 1, name: "1" }, { name: "2", ID: 2 }];
            var tmissingprop = [{ id: 1, name: "1" }, { id: 2 }];

            log("This is ok even though the props are in a different order:");
            oq(jSQL.qry().from(tok));
            try { oq(jSQL.qry().from(tbad)); } catch (e) { logError(e, "misnamed prop"); }
            try { oq(jSQL.qry().from(tmissingprop)); } catch (e) { logError(e, "missing prop"); }

            var nums = [1, 2, 3, 4, 5];
            var oGood = [{ id: 1, name: "a" }, { id: 2, name: "b" }, { id: 4, name: "d" }];
            var oBad = [{ id: 1, name: "a" }, [1, 2, 3], { id: 4, name: "d" }];
            var oiGood = [{ id: 1, name: "a" }, { id: 2, name: "b" }, { id: 4, name: "d" }];
            var oiBad = [{ id: 1, name: "a" }, { id: 2, name: "b" }, 10, { id: 4, name: "d" }];

            try { jSQL.qry().join(oGood, "og", function (x) { }).toArray() } catch (e) { logError(e, "Join and no from table"); }
            try { jSQL.qry().from(oGood).from(oGood, "og").toArray() } catch (e) { logError(e, "2 from tables"); }

            try { jSQL.qry().from().toArray() } catch (e) { logError(e, "No from table"); }
            try { jSQL.qry().from(oGood, "t1").join().toArray() } catch (e) { logError(e, "No join table"); }

            try { jSQL.qry({ validateTableItems: false }).from(nums).toArray() } catch (e) { logError(e, "from an array of nums"); }
            try { jSQL.qry({ validateTableItems: false }).from(oGood, "t1").join(nums, "nums").toArray() } catch (e) { logError(e, "join an array of nums"); }

            try { jSQL.qry().from(oBad).toArray() } catch (e) { logError(e, "from part of a table is not object"); }
            try { jSQL.qry().from(oGood, "t1").join(oBad, "oBad").toArray() } catch (e) { logError(e, "join part of table is not object"); }

            try { jSQL.qry().from(oGood, "t1").join(oiGood, "", null).toArray() } catch (e) { logError(e, "No join alias"); }
            try { jSQL.qry().from(oGood, 10).toArray() } catch (e) { logError(e, "Invalid table alias"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood).toArray() } catch (e) { logError(e, "Missing join alias"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood, 10, null).toArray() } catch (e) { logError(e, "Invalid join alias"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood, "oi").toArray() } catch (e) { logError(e, "Missing join on"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood, "oi", null).toArray() } catch (e) { logError(e, "Invalid join on"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood, "oi", 10).toArray() } catch (e) { logError(e, "Invalid join on"); }
            try { jSQL.qry().from(oGood, "t1").join(oiGood, "oi", [1, 2]).toArray() } catch (e) { logError(e, "Invalid join on"); }

        }

        function testToScalar() {
            logH('testToScalar()');

            var orders = [{ id: 1, name: "a" }, { id: 2, name: "b" }, { id: 3, name: "c" }, { id: 4, name: "d" }];
            var qry = jSQL.qry().from(orders).where(function (x) { return x.id === 2 || x.id === 4; });
            outputQry(["2: " + qry.find(true)]);
            outputQry(["b: " + qry.find("name")]);
            outputQry(["2: " + qry.find("id")]);
            outputQry(["null: " + jSQL.qry().from(orders).where(function (x) { return x.id === 100; }).find("id")]);
            outputQry(["null: " + jSQL.qry().from(orders).where(function (x) { return x.id === 100; }).find(true)]);
            outputQry(["invalid:"]);
            try { qry.find("xxx"); } catch (e) { logError(e); }
        }

        function testInvalidFilterRuntime() {
            logH('testInvalidFilterRuntime()');

            var qry;
            var orders = [{ id: 1, name: "a", p2: 1 }, { id: 1, name: "b", p2: 2 }, { id: 1, name: "b", p2: 3 }];

            // Ensure selector is working.
            //outputQry(jSQL.qry().from(orders)
            //              .where(function (x) { return x.id === 1; }, function (x) { return x.name === "b"; })
            //              .where(function (x) { return x.p2 === 3; })
            //              );
            //return;

            // Non existant property
            try {
                outputQry(jSQL.qry().from(orders).where("x.t99.ID === 1 || x.id === 3"));
            } catch (e) { logError(e); }

            // Specified = instead of == or ===
            try {
                outputQry(jSQL.qry().from(orders).where("x.id = 3"));
            } catch (e) { logError(e); }

            // Only one argument so no sub message required.
            try {
                outputQry(jSQL.qry().from(orders).where(function (x) { x.id === 'no_return' }));
            } catch (e) { logError(e); }

            // 2 arguments so requires sub message.
            try {
                jSQL.qry().from(orders).where(function (x) { return x.id === 1 }, function (x) { x.id === 'no_return' }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).where(function (x) { return x.id === 1 }, function (x) { return undefined }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).where(function (x) { return x.id === 1 }, function (x) { return NaN }).toArray();
            } catch (e) { logError(e); }

            return;

            outputQry(jSQL.qry().from(orders)
                    .select("x.id").select("x.name"));
            return;

            outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where(function (x) { return x.name === "b" }));

            try {
                outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where(function (x) { x.id === 1 }));
            } catch (e) { logError(e); }

        }

        function testInvalidFilterPreRuntime() {
            logH('testInvalidFilterPreRuntime()');

            var qry;
            var orders = [{ id: 1, name: "a" }, { id: 1, name: "b" }];

            try {
                outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where());
            } catch (e) { logError(e); }

            try {
                outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where(function (x) { return x.id === 1 }, undefined, function (x) { return x.id === 1 }));
            } catch (e) { logError(e); }

            try {
                outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where(function (x) { return x.id === 1 }, NaN, function (x) { return x.id === 1 }));
            } catch (e) { logError(e); }

            try {
                outputQry(jSQL.qry().from(orders)
                    .where(function (x) { return x.id === 1 })
                    .where(function (x) { return x.id === 1 }, true, function (x) { return x.id === 1 }));
            } catch (e) { logError(e); }

            try {
                outputQry(jSQL.qry().from(orders).where([1, 2]));
            } catch (e) { logError(e); }

        }

        function testInvalidSelectorErrorRunTime() {
            logH('testInvalidSelectorRuntime()');

            var qry;

            var orderItems = [{ id: 1 }, { id: 2 }];

            try { jSQL.qry().from(orderItems).select(function (x) { return 0 / 0; }).toArray(); } catch (e) { logError(e); }
            try { jSQL.qry().from(orderItems).select(function (x) { return 1 / 0; }).toArray(); } catch (e) { logError(e); }
            try { jSQL.qry().from(orderItems).select(function (x) { return -1 / 0; }).toArray(); } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orderItems).select("x.ID").toArray();
            } catch (e) { logError(e); }
            try {
                jSQL.qry().from(orderItems).select({ id: "x.ID" }).toArray();
            } catch (e) { logError(e); }
            try {
                jSQL.qry().from(orderItems).select("x.o.id").toArray();
            } catch (e) { logError(e); }
            try {
                jSQL.qry().from(orderItems).select({ id: "x.o.id" }).toArray();
            } catch (e) { logError(e); }

            // function type selector contains a sub function.
            try {
                jSQL.qry().from(orderItems).select(function (x) { return { id: x.id, p2: function () { return x.id; } }; }).toArray();
            } catch (e) { logError(e); }

            // NaN 1:
            try {
                jSQL.qry().from(orderItems, "oi").select({ wrong: function (x) { return x.www * x.oi.id } }).toArray();
            } catch (e) { logError(e); }

            // NaN 2:
            try {
                jSQL.qry().from(orderItems, "oi").select(function (x) { return { wrong: x.www * x.oi.id } }).toArray();
            } catch (e) { logError(e); }

            // Computation error 1:
            try {
                jSQL.qry().from(orderItems, "oi").select({ wrong: function (x) { return x.oi.id.toString() + x.www.toString() } }).toArray();
            } catch (e) { logError(e); }

            // Computation error 2:
            try {
                jSQL.qry().from(orderItems, "oi").select(function (x) { return { wrong: x.oi.id.toString() + x.www.toString() } }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orderItems, "oi").select("x.oi.www").toArray();
            } catch (e) { logError(e); }

            //**

            try {
                jSQL.qry().from(orderItems, "oi").select(function (x) { return { id: x.oi.id, wrong: x.www }; }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orderItems, "oi").select({ x1: "x.oi.www", x2: "x.oi.id" }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orderItems, "oi").select({ x2: function (x) { return x.oi.www } }).toArray();
            } catch (e) { logError(e); }

            // Forget the return part of a function 1.
            try {
                jSQL.qry().from(orderItems, "oi").select({ id: function (x) { x.oi.id } }).toArray();
            } catch (e) { logError(e); }

            // Forget the return part of a function 2.
            try {
                jSQL.qry().from(orderItems, "oi").select(function (x) { id: x.oi.id }).toArray();
            } catch (e) { logError(e); }

        }

        function testInvalidSelectorErrorPreRunTime() {
            logH('testInvalidSelectorPreRuntime()');

            var qry;
            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }];

            try { jSQL.qry().from(orders).select(0 / 0).toArray(); } catch (e) { logError(e); }
            try { jSQL.qry().from(orders).select(1 / 0).toArray(); } catch (e) { logError(e); }
            try { jSQL.qry().from(orders).select(-1 / 0).toArray(); } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum("x.id", undefined).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum().toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum(10, "x.y", "*").toArray();
            } catch (e) { logError(e); }

            try {
                outputQry(jSQL.qry().from(orders).sum({ id: "x.id", all: "*" }).toArray());
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum(10, "x.y", undefined).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum(NaN, 10, "x.y").toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum(10, 20, { id: "x.id", total: undefined }).toArray();
            } catch (e) { logError(e); }

            try {
                jSQL.qry().from(orders).sum({ id: "x.id", total: NaN }).toArray();
            } catch (e) { logError(e); }

        }

        function testAggConstants() {
            logH('testAggConstants()');

            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }];
            outputQry(jSQL.qry().from(orders).sum("x.id", 1000, function () { return "x_"; }).count(1000).toArray());
            outputQry(jSQL.qry().from(orders).sum("x.id", 1000, function () { return { concat: "x_" }; }).count(1000).toArray());
            outputQry(jSQL.qry().from(orders).sum({ sum_id: "x.id", sum_1000: 1000, concat: function () { return "x_"; } })
                                       .count({ count_1000: 1000 }).toArray());
            outputQry(jSQL.qry().from(orders).sum(function (x) { return { sum_id: x.id, sum_1000: 1000, concat: "x_" } })
                                       .count(function (x) { return { count_1000: 1000 } }).toArray());
        }

        function testReturnConstant() {
            logH('testReturnConstant()');

            var qry;
            var orders = [{ id: 1 }];

            outputQry(jSQL.qry().from(orders).select(function (x) { return { id: x.id, fixed: "constant" } }));
            outputQry(jSQL.qry().from(orders).select({ id: "x.id", fixed: function () { return "constant"; } }));
            outputQry(jSQL.qry().from(orders).select(function (x) { return { id: x.id, fixed: null } }));
            outputQry(jSQL.qry().from(orders).select({ id: "x.id", fixed: function () { return null; } }));
            outputQry(jSQL.qry().from(orders).select({ id: "x.id", fixed: null }));
            outputQry(jSQL.qry().from(orders).select(function (x) { return { id: x.id, fixed: [1, 2] } }));
            outputQry(jSQL.qry().from(orders).select({ id: "x.id", fixed: function () { return [1, 2]; } }));
            outputQry(jSQL.qry().from(orders).select({ id: "x.id", fixed: [1, 2] }));
            outputQry(jSQL.qry().from(orders).select("x.id", function () { return "constant"; }));
            outputQry(jSQL.qry().from(orders).select("x.id", null));
            outputQry(jSQL.qry().from(orders).select("x.id", function () { return null; }));
            outputQry(jSQL.qry().from(orders).select("x.id", [1, 2]));
            outputQry(jSQL.qry().from(orders).select("x.id", function () { return [1, 2]; }));
        }

        function testCountAll() {
            logH('testCountAll()');

            var qry;
            var orders = [{ id: 1, group: 1, name: "a" }, { id: 2, group: 1, name: null },
                          { id: 3, group: 2, name: "b" }, { id: 4, group: 2, name: null }, { id: 5, group: 2, name: "c" }];
            qry = jSQL.qry().from(orders)
                      .count()
                      .count("*")
                      .count({ count_All: "*", count_All_2: function () { return "*"; } })
                      .count(function (x) { return { count_All_3: "*" }; })
                      .count("*", "x.name", "x.id")
            ;
            outputQry(orders);
            outputQry(qry);
        }

        function testCount() {
            logH('testCount()');

            var qry;
            var orders = [{ id: 1, group: 1, name: "a" }, { id: 2, group: 1, name: null },
                          { id: 3, group: 2, name: "b" }, { id: 4, group: 2, name: null }, { id: 5, group: 2, name: "c" }];
            qry = jSQL.qry().from(orders)
                      .count("x.id").count("x.name")
                      .count({ count_id2: "x.id", count_name2: "x.name" })
                      .count({ count_id3: function (x) { return x.id; }, count_name3: function (x) { return x.name; } })
                      .count(function (x) { return { count_id4: x.id, count_name4: x.name }; });
            outputQry(orders);
            outputQry(qry);

            qry = jSQL.qry().from(orders)
                      .groupBy({ group: "x.group" })
                      .count("x.id").count("x.name")
                      .count({ count_id2: "x.id", count_name2: "x.name" })
                      .count({ count_id3: function (x) { return x.id; }, count_name3: function (x) { return x.name; } })
                      .count(function (x) { return { count_id4: x.id, count_name4: x.name }; });
            outputQry(qry);
        }

        function testAggNoGroupNoJoin(having) {
            logH('testAggNoGroupNoJoin()');

            var qry;
            var orderItems = [{ id: 1000, orderId: 10, quantity: 1 }, { id: 1001, orderId: 10, quantity: 2 },
                              { id: 1002, orderId: 11, quantity: 3 }, { id: 1003, orderId: 11, quantity: 4 },
                              { id: 1004, orderId: 12, quantity: 5 }, { id: 1005, orderId: 12, quantity: 6 }];

            if (having) {
                log("HAVING " + having.toString());
            }

            log("NO ALIAS. totalQty=21");
            qry = jSQL.qry().from(orderItems).sum(function (x) { return { totalQ: x.quantity }; })
            .sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());

            log("WITH ALIAS. totalQty=21");
            qry = jSQL.qry().from(orderItems, "oi").sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());

            log("NO ALIAS, WHERE. totalQty=6");
            qry = jSQL.qry().from(orderItems).where(function (x) { return x.id < 1003; })
            .sum(function (x) { return { totalQ: x.quantity }; })
            .sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());

            log("WITH ALIAS, WHERE. totalQty=6");
            qry = jSQL.qry().from(orderItems, "oi").where(function (x) { return x.oi.id < 1003; })
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());
        }

        function testAggNoGroupWithJoin(having) {
            logH('testAggNoGroupWithJoin()');

            var qry;

            if (having) {
                log("HAVING " + having.toString());
            }

            var orders = [{ id: 2 }, { id: 4 }];
            var orderItems = [{ id: 10, orderId: 1, quantity: 1 }, { id: 11, orderId: 1, quantity: 2 },
                              { id: 20, orderId: 2, quantity: 3 }, { id: 21, orderId: 2, quantity: 4 },
                              { id: 30, orderId: 3, quantity: 5 }, { id: 31, orderId: 3, quantity: 6 },
                              { id: 40, orderId: 4, quantity: 7 }, { id: 41, orderId: 4, quantity: 8 }];

            log("NO WHERE. totalQty=22");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());

            log("WHERE. totalQty=18");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .where(function (x) { return x.oi.quantity !== 4 })
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            if (having) {
                qry = qry.having(function (x) { return x.totalQty === having; });
            }
            outputQry(qry.toArray());

        }

        function testAggGroupNoJoin() {
            logH('testAggGroupNoJoin()');

            var qry;
            var orderItems = [{ id: 1000, orderId: 10, quantity: 1 }, { id: 1001, orderId: 10, quantity: 2 },
                              { id: 1002, orderId: 11, quantity: 3 }, { id: 1003, orderId: 11, quantity: 4 },
                              { id: 1004, orderId: 12, quantity: 5 }, { id: 1005, orderId: 12, quantity: 6 }];

            log("NO WHERE. totalQty=3, 7, 11");
            qry = jSQL.qry().from(orderItems).groupBy("x.orderId")
            .sum(function (x) { return { totalQ: x.quantity }; }).sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]");
            outputQry(qry.toArray());

            log("WITH WHERE. totalQty=3, 3, 11");
            qry = jSQL.qry().from(orderItems).where(function (x) { return x.quantity !== 4; })
            .groupBy("x.orderId")
            .sum(function (x) { return { totalQ: x.quantity }; }).sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]");
            outputQry(qry.toArray());

            log("NO WHERE, WITH HAVING. totalQty=3, 11");
            qry = jSQL.qry().from(orderItems).groupBy("x.orderId")
            .sum(function (x) { return { totalQ: x.quantity }; }).sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]")
            .having(function (x) { return x.totalQty === 3 || x.totalQty === 11; });
            outputQry(qry.toArray());

            log("WITH WHERE, WITH HAVING. totalQty=3, 3");
            qry = jSQL.qry().from(orderItems).where(function (x) { return x.quantity !== 4; })
            .groupBy("x.orderId")
            .sum(function (x) { return { totalQ: x.quantity }; }).sum({ totalQty2: "x.quantity" }).sum("x.quantity[totalQty]").avg("x.quantity[avgQty]")
            .having(function (x) { return x.totalQty === 3; });
            outputQry(qry.toArray());
        }

        function testAggGroupWithJoin() {
            logH('testAggGroupWithJoin()');

            var qry;

            var orders = [{ id: 2 }, { id: 4 }, { id: 5 }];
            var orderItems = [{ id: 10, orderId: 1, quantity: 1 }, { id: 11, orderId: 1, quantity: 2 },
                              { id: 20, orderId: 2, quantity: 3 }, { id: 21, orderId: 2, quantity: 4 },
                              { id: 30, orderId: 3, quantity: 5 }, { id: 31, orderId: 3, quantity: 6 },
                              { id: 40, orderId: 4, quantity: 7 }, { id: 41, orderId: 4, quantity: 8 },
                              { id: 50, orderId: 5, quantity: 9 }, { id: 51, orderId: 5, quantity: 10 }];

            log("NO WHERE. totalQty=7, 15, 19");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .groupBy("x.oi.orderId")
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            outputQry(qry.toArray());

            log("WITH WHERE. totalQty=3, 15, 10");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .where(function (x) { return x.oi.quantity !== 4 && x.oi.quantity !== 9; })
            .groupBy("x.oi.orderId")
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]");
            outputQry(qry.toArray());

            log("NO WHERE, WITH HAVING. totalQty=7, 19");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .groupBy("x.oi.orderId")
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]")
            .having(function (x) { return x.totalQty !== 15; });
            outputQry(qry.toArray());

            log("WITH WHERE, WITH HAVING. totalQty=3, 10");
            qry = jSQL.qry().from(orders, "o").join(orderItems, "oi", function (x) { return x.oi.orderId === x.o.id; })
            .where(function (x) { return x.oi.quantity !== 4 && x.oi.quantity !== 9; })
            .groupBy("x.oi.orderId")
            .sum(function (x) { return { totalQ: x.oi.quantity }; })
            .sum({ totalQty2: "x.oi.quantity" }).sum("x.oi.quantity[totalQty]").avg("x.oi.quantity[avgQty]")
            .having(function (x) { return x.totalQty !== 15; });
            outputQry(qry.toArray());
        }

        function testSelectAll() {
            logH('testSelectAll()');

            var qry;
            var orders = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
            var orderItems = [{ id: 10, orderId: 1, qty: 5 }, { id: 11, orderId: 1, qty: 6 }];

            //qry = jSQL.qry().from(orders)
            //          .join(orderItems, "oi", function (x) { return x.id === x.oi.orderId; })
            //.select({ orderItemId: "oi.id" }, { qty: "oi.qty" }, "*");

            //outputQry([qry.toArray()]);

            qry = jSQL.qry().from(orders, "o")
                      .join(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                      .select("*");
            outputQry([qry.toArray()]);
        }

        function testOrderByNoEffectOriginal() {
            logH('testOrderByNoEffectOriginal()');

            var qry;
            var orderItems = [{ id: 1, qty: 3, price: 10.25 }, { id: 2, qty: 10, price: 5.5 },
                              { id: 3, qty: 7, price: 1.5 }, { id: 4, qty: 1, price: 10.25 }, { id: 99, qty: 1, price: 9999 }];

            outputQry(["Original"]);
            outputQry(orderItems);

            outputQry(["Sorted"]);
            qry = jSQL.qry().from(orderItems)
                      .orderBy("x.price").orderBy("x.qty");
            outputQry(qry.toArray());

            outputQry(["Original"]);
            outputQry(orderItems);
        }

        function testTableSelect() {
            logH('testTableSelect()');

            var qry;
            var orderItems = [{ id: 1, qty: 3, price: 10.25 }, { id: 2, qty: 10, price: 5.5 },
                              { id: 3, qty: 7, price: 1.5 }, { id: 4, qty: 1, price: 55 }, { id: 99, qty: 1, price: 9999 }];

            qry = jSQL.qry().from(orderItems, "oi")
                      .select({
                          id: "x.oi.id", qty: "x.oi.qty", price: "x.oi.price",
                          total: function (x) { return x.oi.price * x.oi.qty }
                      })
                      .where(function (x) { return x.oi.id !== 99; })
                      .orderBy("x.total").orderBy("x.qty");
            outputQry(qry.toArray());

            qry = jSQL.qry().from(orderItems)
                      .select({
                          id: "x.id", qty: "x.qty", price: "x.price",
                          total: function (x) { return x.price * x.qty }
                      })
                      .where(function (x) { return x.id !== 99; })
                      .orderBy("x.total", "x.qty");
            outputQry(qry.toArray());
        }

        function testGroupByHaving() {
            logH('testGroupByHaving');

            var qry;
            var orders = [{ id: 10, address: "13 street" },
                          { id: 11, address: "20 acacia av." },
                          { id: 12, address: "whereever." },
                          { id: 13, address: "order 13" }];

            var orderItems = [{ id: 1000, orderId: 10, qty: 3 }, { id: 1001, orderId: 10, qty: 2 },
                              { id: 1002, orderId: 11, qty: 4 }, { id: 1003, orderId: 11, qty: 25 },
                              { id: 1004, orderId: 12, qty: 10 }, { id: 1005, orderId: 12, qty: 85 },
                              { id: 1006, orderId: 12, qty: 25 },
                              { id: 1300, orderId: 13, qty: 10 }, { id: 1301, orderId: 13, qty: 85 }];

            qry = jSQL.qry().from(orders, "o")
                     .join(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                     .where(function (x) { return x.o.id < 13 && x.oi.id !== 1005; })
                     .groupBy(function (x) { return { id: x.o.id, address: x.o.address } })
                     .sum(function (x) { return { totalQty: x.oi.qty }; })
                     .min(function (x) { return { minQty: x.oi.qty }; })
                     .max(function (x) { return { maxQty: x.oi.qty }; })
                     .sum({ totalQty2: "x.oi.qty" })
                     .min({ minQty2: "x.oi.qty" })
                     .max({ maxQty2: "x.oi.qty" })
                     .having(function (x) { return x.maxQty2 === 25 }, function (x) { return x.minQty === 4 })
            ;

            outputQry(qry.toArray());
        }

        function testGroupBy() {
            logH('testGroupBy');

            var qry;
            var orders = [{ id: 10, address: "13 street" }, { id: 11, address: "20 acacia av." },
                          { id: 12, address: "whereever." }, { id: 13, address: "no order item." }];

            var orderItems = [{ id: 1000, orderId: 10, qty: 3 }, { id: 1001, orderId: 10, qty: 2 },
                              { id: 1002, orderId: 11, qty: 4 }, { id: 1003, orderId: 11, qty: 25 },
                              { id: 1004, orderId: 12, qty: 10 }, { id: 1005, orderId: 12, qty: 85 }];

            qry = jSQL.qry().from(orders, "o")
                     .leftjoin(orderItems, "oi", function (x) { return x.o.id === x.oi.orderId; })
                     .groupBy(function (x) { return { id: x.o.id, address: x.o.address } })
                     .count("x.o.id[Count_id]")
                     .sum(function (x) { return { totalQty: x.oi.qty }; })
                     .min(function (x) { return { minQty: x.oi.qty }; })
                     .max(function (x) { return { maxQty: x.oi.qty }; })
                     .sum({ totalQty2: "x.oi.qty" })
                     .min({ minQty2: "x.oi.qty" })
                     .max({ maxQty2: "x.oi.qty" })
                     .max({ maxQty3: function (x) { return x.oi.qty; } })
                     .agg(function (a) { return a.count === 0 ? 0 : a.total / a.count; },
                            { avgQty: "x.oi.qty" }, { avgOrderItemItem: "x.oi.id" })
            ;

            outputQry(qry.toArray());
        }

        function logError(e, msg) {
            if (msg) {
                msg = "[" + msg + "]: " + e.name + ": " + e.message;
            }
            else {
                msg = e.name + ": " + e.message;
            }
            log(msg);
        }

        function log(msg) {
            msg += "";
            msg = msg.split("\n").join("<br />");
            append("<p>" + msg + "</p>");
        }

        function logH(msg) {
            msg += "";
            msg = msg.split("\n").join("<br />");
            append("<p style='color:green;font-weight:bold'>" + msg + "</p>");
        }

        function outputQry(qry) {
            var sb = "";
            if (qry.length === undefined) {
                qry = qry.toArray();
            }
            if (qry.length === 0) {
                append("{none}");
                return;
            }
            for (var i = 0; i < qry.length; i++) {
                if (sb.length > 0) {
                    sb += "<br />";
                }
                sb += JSON.stringify(qry[i]);
            }
            append("<p>" + sb + "</p>");
        }

        var hashString = function (str) {
            var h = 0, i = 0, l = str.length;
            if (l === 0) return h;
            for (; i < l; i++) {
                h = ((h << 5) - h) + str.charCodeAt(i);
                h |= 0; // Convert to 32bit integer
            }
            return h;
        };

        function append(html) {
            results.innerHTML += html;
        }

    </script>

</body>
</html>